﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Numerics;
using System.Diagnostics;
using System.IO;

namespace BitcoinLibrary
{
    /**
    * A transaction represents the movement of coins from some addresses to some other addresses. It can also represent
    * the minting of new coins. A Transaction object corresponds to the equivalent in the BitCoin C++ implementation.<p>
    *
    * It implements TWO serialization protocols - the BitCoin proprietary format which is identical to the C++
    * implementation and is used for reading/writing transactions to the wire and for hashing. It also implements Java
    * serialization which is used for the wallet. This allows us to easily add extra fields used for our own accounting
    * or UI purposes.
    */
    public class Transaction : Message /*implements Serializable*/ {
        //private static readonly Logger log = LoggerFactory.getLogger(Transaction.class);
        private const long serialVersionUID = -8567546957352643140L;

        // These are serialized in both bitcoin and java serialization.
        long version;
        public List<TransactionInput> inputs;
        public List<TransactionOutput> outputs;
        long lockTime;

        // This is only stored in Java serialization. It records which blocks (and their height + work) the transaction
        // has been included in. For most transactions this set will have a single member. In the case of a chain split a
        // transaction may appear in multiple blocks but only one of them is part of the best chain. It's not valid to
        // have an identical transaction appear in two blocks in the same chain but this invariant is expensive to check,
        // so it's not directly enforced anywhere.
        //
        // If this transaction is not stored in the wallet, appearsIn is null.
        //Set<StoredBlock> appearsIn;
        public HashSet<StoredBlock> appearsIn;

        // This is an in memory helper only.
        private Sha256Hash hash;

        public Transaction(NetworkParameters param)
            : base(param)
        {
            version = 1;
            inputs = new List<TransactionInput>();
            outputs = new List<TransactionOutput>();
            // We don't initialize appearsIn deliberately as it's only useful for transactions stored in the wallet.
        }

        /**
        * Creates a transaction from the given serialized bytes, eg, from a block or a tx network message.
        */
        public Transaction(NetworkParameters param, byte[] payloadBytes)
            : base(param, payloadBytes, 0)
        {
        }

        /**
        * Creates a transaction by reading payload starting from offset bytes in. Length of a transaction is fixed.
        */
        public Transaction(NetworkParameters param, byte[] payload, int offset)
            : base(param, payload, offset)
        {
            // inputs/outputs will be created in parse()
        }

        /**
        * Returns a read-only list of the inputs of this transaction.
        */
        public /*readonly*/ List<TransactionInput> getInputs()
        {
            return inputs;  // TODO: replace with ToArray()?
        }

        /**
        * Returns the transaction hash as you see them in the block explorer.
        */
        public Sha256Hash getHash()
        {
            if (hash == null)
            {
                byte[] bits = bitcoinSerialize();
                Debug.WriteLine(Utils.GetEclipseArray(bits));
                hash = new Sha256Hash(Utils.reverseBytes(Utils.doubleDigest(bits)));
            }
            return hash;
        }

        public string getHashAsString()
        {
            return getHash().ToString();
        }

        void setFakeHashForTesting(Sha256Hash hash)
        {
            this.hash = hash;
        }

        /**
        * Calculates the sum of the outputs that are sending coins to a key in the wallet. The flag controls whether to
        * include spent outputs or not.
        */
        public BigInteger getValueSentToMe(Wallet wallet, bool includeSpent)
        {
            // This is tested in WalletTest.
            BigInteger v = BigInteger.Zero;
            foreach (TransactionOutput o in outputs)
            {
                if (!o.isMine(wallet)) continue;
                if (!includeSpent && !o.isAvailableForSpending()) continue;
                v = v + o.getValue();
            }
            return v;
        }

        /**
        * Calculates the sum of the outputs that are sending coins to a key in the wallet.
        */
        public BigInteger getValueSentToMe(Wallet wallet)
        {
            return getValueSentToMe(wallet, true);
        }

        /**
        * Returns a set of blocks which contain the transaction, or null if this transaction doesn't have that data
        * because it's not stored in the wallet or because it has never appeared in a block.
        */
        public HashSet<StoredBlock> getAppearsIn()
        {
            return appearsIn;
        }

        /**
        * Adds the given block to the internal serializable set of blocks in which this transaction appears. This is
        * used by the wallet to ensure transactions that appear on side chains are recorded properly even though the
        * block stores do not save the transaction data at all.
        */
        public void addBlockAppearance(StoredBlock block)
        {
            if (appearsIn == null)
            {
                appearsIn = new HashSet<StoredBlock>();
            }
            appearsIn.Add(block);
        }

        /**
        * Calculates the sum of the inputs that are spending coins with keys in the wallet. This requires the
        * transactions sending coins to those keys to be in the wallet. This method will not attempt to download the
        * blocks containing the input transactions if the key is in the wallet but the transactions are not.
        *
        * @return sum in nanocoins.
        */
        public BigInteger getValueSentFromMe(Wallet wallet)
        {
            // This is tested in WalletTest.
            BigInteger
                v = BigInteger.Zero;
            foreach (TransactionInput input in inputs)
            {
                // This input is taking value from an transaction in our wallet. To discover the value,
                // we must find the connected transaction.
                TransactionOutput connected = input.getConnectedOutput(wallet.unspent);
                if (connected == null)
                    connected = input.getConnectedOutput(wallet.spent);
                if (connected == null)
                    connected = input.getConnectedOutput(wallet.pending);
                if (connected == null)
                    continue;
                v = v + connected.getValue();
            }
            return v;
        }

        public bool disconnectInputs()
        {
            bool disconnected = false;
            foreach (TransactionInput input in inputs)
            {
                disconnected |= input.disconnect();
            }
            return disconnected;
        }

        /**
        * Connects all inputs using the provided transactions. If any input cannot be connected returns that input or
        * null on success.
        */
        public TransactionInput connectInputs(Dictionary<Sha256Hash, Transaction> transactions, bool disconnect)
        {
            foreach (TransactionInput input in inputs)
            {
                // Coinbase transactions, by definition, do not have connectable inputs.
                if (input.isCoinBase()) continue;
                if (input.connect(transactions, disconnect) != TransactionInput.ConnectionResult.SUCCESS)
                {
                    // Could not connect this input, so return it and abort.
                    return input;
                }
            }
            return null;
        }

        /**
        * These constants are a part of a scriptSig signature on the inputs. They define the details of how a
        * transaction can be redeemed, specifically, they control how the hash of the transaction is calculated.
        * 
        * In the official client, this enum also has another flag, SIGHASH_ANYONECANPAY. In this implementation,
        * that's kept separate. Only SIGHASH_ALL is actually used in the official client today. The other flags
        * exist to allow for distributed contracts.
        */
        public enum SigHash
        {
            ALL, // 1
            NONE, // 2
            SINGLE, // 3
        }

        public override void parse()
        {
            version = readUint32();
            // First come the inputs.
            long numInputs = readVarInt();
            inputs = new List<TransactionInput>((int)numInputs);
            for (long i = 0; i < numInputs; i++)
            {
                TransactionInput input = new TransactionInput(param, this, bytes, cursor);
                inputs.Add(input);
                cursor += input.getMessageSize();
            }
            // Now the outputs
            long numOutputs = readVarInt();
            outputs = new List<TransactionOutput>((int)numOutputs);
            for (long i = 0; i < numOutputs; i++)
            {
                TransactionOutput output = new TransactionOutput(param, this, bytes, cursor);
                outputs.Add(output);
                cursor += output.getMessageSize();
            }
            lockTime = readUint32();

            // Store a hash, it may come in useful later (want to avoid reserialization costs).
            hash = new Sha256Hash(Utils.reverseBytes(Utils.doubleDigest(bytes, offset, cursor - offset)));
        }

        /**
        * A coinbase transaction is one that creates a new coin. They are the first transaction in each block and their
        * value is determined by a formula that all implementations of BitCoin share. In 2011 the value of a coinbase
        * transaction is 50 coins, but in future it will be less. A coinbase transaction is defined not only by its
        * position in a block but by the data in the inputs.
        */
        public bool isCoinBase()
        {
            return inputs[0].isCoinBase();
        }

        /**
        * @return A human readable version of the transaction useful for debugging.
        */
        public override string ToString()
        {
            StringBuilder s = new StringBuilder();
            s.Append(" ");
            s.Append(getHashAsString());
            s.Append("\n");
            if (isCoinBase())
            {
                String script = "???";
                String script2 = "???";
                try
                {
                    script = inputs[0].getScriptSig().ToString();
                    script2 = outputs[0].getScriptPubKey().ToString();
                }
                catch (Exception e)
                {
                }
                return " == COINBASE TXN (scriptSig " + script + ") (scriptPubKey " + script2 + ")";
            }
            foreach (TransactionInput ti in inputs)
            {
                s.Append(" ");
                s.Append("from ");

                try
                {
                    s.Append(ti.getScriptSig().getFromAddress().ToString());
                }
                catch (Exception e)
                {
                    s.Append("[exception: ").Append(e.Message).Append("]");
                    throw e;
                }
                s.Append("\n");
            }
            foreach (TransactionOutput to in outputs)
            {
                s.Append(" ");
                s.Append("to ");
                try
                {
                    Address toAddr = new Address(param, to.getScriptPubKey().getPubKeyHash());
                    s.Append(toAddr.ToString());
                    s.Append(" ");
                    s.Append(Utils.bitcoinValueToFriendlyString(to.getValue()));
                    s.Append(" BTC");
                }
                catch (Exception e)
                {
                    s.Append("[exception: ").Append(e.Message).Append("]");
                }
                s.Append("\n");
            }
            return s.ToString();
        }

        /**
        * Adds an input to this transaction that imports value from the given output. Note that this input is NOT
        * complete and after every input is added with addInput() and every output is added with addOutput(),
        * signInputs() must be called to readonlyize the transaction and finish the inputs off. Otherwise it won't be
        * accepted by the network.
        */
        public void addInput(TransactionOutput from)
        {
            inputs.Add(new TransactionInput(param, this, from));
        }

        /**
        * Adds the given output to this transaction. The output must be completely initialized.
        */
        public void addOutput(TransactionOutput to)
        {
            to.parentTransaction = this;
            outputs.Add(to);
        }

        /**
        * Once a transaction has some inputs and outputs added, the signatures in the inputs can be calculated. The
        * signature is over the transaction itself, to prove the redeemer actually created that transaction,
        * so we have to do this step last.<p>
        *
        * This method is similar to SignatureHash in script.cpp
        *
        * @param hashType This should always be set to SigHash.ALL currently. Other types are unused.
        * @param wallet A wallet is required to fetch the keys needed for signing.
        */
        public void signInputs(SigHash hashType, Wallet wallet)
        {
            Debug.Assert(inputs.Count > 0);
            Debug.Assert(outputs.Count > 0);

            // I don't currently have an easy way to test other modes work, as the official client does not use them.
            Debug.Assert(hashType == SigHash.ALL);

            // The transaction is signed with the input scripts empty except for the input we are signing. In the case
            // where addInput has been used to set up a new transaction, they are already all empty. The input being signed
            // has to have the connected OUTPUT program in it when the hash is calculated!
            //
            // Note that each input may be claiming an output sent to a different key. So we have to look at the outputs
            // to figure out which key to sign with.

            byte[][] signatures = new byte[inputs.Count][];
            ECKey[] signingKeys = new ECKey[inputs.Count];
            for (int i = 0; i < inputs.Count; i++)
            {
                TransactionInput input = inputs[i];
                Debug.Assert(input.scriptBytes.Length == 0, "Attempting to sign a non-fresh transaction");
                // Set the input to the script of its output.
                input.scriptBytes = input.outpoint.getConnectedPubKeyScript();
                // Find the signing key we'll need to use.
                byte[] connectedPubKeyHash = input.outpoint.getConnectedPubKeyHash();
                ECKey key = wallet.findKeyFromPubHash(connectedPubKeyHash);
                // This assert should never fire. If it does, it means the wallet is inconsistent.
                Debug.Assert(key != null, "Transaction exists in wallet that we cannot redeem: " + Utils.bytesToHexString(connectedPubKeyHash));
                // Keep the key around for the script creation step below.
                signingKeys[i] = key;
                // The anyoneCanPay feature isn't used at the moment.
                bool anyoneCanPay = false;
                byte[] hash = hashTransactionForSignature(hashType, anyoneCanPay);
                // Set the script to empty again for the next input.
                input.scriptBytes = new byte[] { };// TransactionInput.EMPTY_ARRAY;

                // Now sign for the output so we can redeem it. We use the keypair to sign the hash,
                // and then put the resulting signature in the script along with the public key (below).
                try
                {
                    MemoryStream bos = new MemoryStream();
                    var data = key.sign(hash);
                    bos.Write(data, 0, data.Length);
                    bos.WriteByte((byte)(((int)hashType + 1) | (anyoneCanPay ? 0x80 : 0)));
                    signatures[i] = bos.ToArray();
                }
                catch (Exception e)
                {
                    throw e; // Cannot happen.
                }
            }

            // Now we have calculated each signature, go through and create the scripts. Reminder: the script consists of
            // a signature (over a hash of the transaction) and the complete public key needed to sign for the connected
            // output.
            for (int i = 0; i < inputs.Count; i++)
            {
                TransactionInput input = inputs[i];
                Debug.Assert(input.scriptBytes.Length == 0);
                ECKey key = signingKeys[i];
                input.scriptBytes = Script.createInputScript(signatures[i], key.getPubKey());
            }

            // Every input is now complete.
        }

        private byte[] hashTransactionForSignature(SigHash type, bool anyoneCanPay)
        {
            try
            {
                MemoryStream bos = new MemoryStream();
                bitcoinSerializeToStream(bos);
                // We also have to write a hash type.
                int hashType = (int)type + 1;
                if (anyoneCanPay)
                    hashType |= 0x80;
                Utils.uint32ToByteStreamLE(hashType, bos);
                // Note that this is NOT reversed to ensure it will be signed correctly. If it were to be printed out
                // however then we would expect that it is IS reversed.
                return Utils.doubleDigest(bos.ToArray());
            }
            catch (IOException e)
            {
                throw e; // Cannot happen.
            }
        }

        /**
        * Given a named input and the transaction output it connects to, runs the script formed from the
        * concatenation of the input and output scripts, returning true if the link is valid. In
        * this way, we prove that the creator of this transaction is allowed to redeem the output
        * of the connectedTx and thus spend the money.<p>
        *
        * <b>WARNING: NOT FINISHED</b><p>
        * 
        * @param inputIndex Which input to verify.
        * @param connectedTx The Transaction that the input is connected to.
        */
        public bool verifyInput(int inputIndex, Transaction connectedTx)
        {
            TransactionInput input = inputs[inputIndex];
            //int outputIndex = (int) input.outpoint.index;
            //assert outputIndex >= 0 && outputIndex < connectedTx.outputs.size();
            //Script outScript = connectedTx.outputs.get(outputIndex).getScriptPubKey();
            Script inScript = input.getScriptSig();
            //Script script = Script.join(inScript, outScript);
            //if (script.run(this)) {
            // LOG("Transaction input successfully verified!");
            // return true;
            //}
            byte[] pubkey = inScript.getPubKey();

            return false;
        }

        public override void bitcoinSerializeToStream(Stream stream)
        {
            Utils.uint32ToByteStreamLE(version, stream);
            var data = new VarInt(inputs.Count).encode();
            stream.Write(data, 0, data.Length);
            foreach (TransactionInput ti in inputs)
                ti.bitcoinSerializeToStream(stream);
            data = new VarInt(outputs.Count).encode();
            stream.Write(data, 0, data.Length);
            foreach (TransactionOutput to in outputs)
                to.bitcoinSerializeToStream(stream);
            Utils.uint32ToByteStreamLE(lockTime, stream);
        }

        public override bool Equals(object other)
        {
            if (!(other is Transaction)) return false;
            Transaction t = (Transaction)other;

            return t.getHash().Equals(getHash());
        }

        public override int GetHashCode()
        {
            return getHash().hash.GetHashCode();    // Arrays.hashCode(getHash().hash);
        }
    }
}
